{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/home/joaquin/Documents/GitHub/skforecast\n",
      "0.19.0\n"
     ]
    }
   ],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2\n",
    "import sys\n",
    "from pathlib import Path\n",
    "path = str(Path.cwd().parent)\n",
    "print(path)\n",
    "sys.path.insert(1, path)\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import skforecast\n",
    "\n",
    "print(skforecast.__version__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from aeon.forecasting.stats import ARIMA, AutoARIMA\n",
    "from pmdarima import auto_arima\n",
    "import numpy as np\n",
    "from statsmodels.tsa.stattools import adfuller, kpss\n",
    "from skforecast.datasets import fetch_dataset\n",
    "import warnings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">╭──────────────────────────────── <span style=\"font-weight: bold\">fuel_consumption</span> ────────────────────────────────╮\n",
       "│ <span style=\"font-weight: bold\">Description:</span>                                                                     │\n",
       "│ Monthly fuel consumption in Spain from 1969-01-01 to 2022-08-01.                 │\n",
       "│                                                                                  │\n",
       "│ <span style=\"font-weight: bold\">Source:</span>                                                                          │\n",
       "│ Obtained from Corporación de Reservas Estratégicas de Productos Petrolíferos and │\n",
       "│ Corporación de Derecho Público tutelada por el Ministerio para la Transición     │\n",
       "│ Ecológica y el Reto Demográfico. https://www.cores.es/es/estadisticas            │\n",
       "│                                                                                  │\n",
       "│ <span style=\"font-weight: bold\">URL:</span>                                                                             │\n",
       "│ https://raw.githubusercontent.com/skforecast/skforecast-                         │\n",
       "│ datasets/main/data/consumos-combustibles-mensual.csv                             │\n",
       "│                                                                                  │\n",
       "│ <span style=\"font-weight: bold\">Shape:</span> 644 rows x 6 columns                                                      │\n",
       "╰──────────────────────────────────────────────────────────────────────────────────╯\n",
       "</pre>\n"
      ],
      "text/plain": [
       "╭──────────────────────────────── \u001b[1mfuel_consumption\u001b[0m ────────────────────────────────╮\n",
       "│ \u001b[1mDescription:\u001b[0m                                                                     │\n",
       "│ Monthly fuel consumption in Spain from 1969-01-01 to 2022-08-01.                 │\n",
       "│                                                                                  │\n",
       "│ \u001b[1mSource:\u001b[0m                                                                          │\n",
       "│ Obtained from Corporación de Reservas Estratégicas de Productos Petrolíferos and │\n",
       "│ Corporación de Derecho Público tutelada por el Ministerio para la Transición     │\n",
       "│ Ecológica y el Reto Demográfico. https://www.cores.es/es/estadisticas            │\n",
       "│                                                                                  │\n",
       "│ \u001b[1mURL:\u001b[0m                                                                             │\n",
       "│ https://raw.githubusercontent.com/skforecast/skforecast-                         │\n",
       "│ datasets/main/data/consumos-combustibles-mensual.csv                             │\n",
       "│                                                                                  │\n",
       "│ \u001b[1mShape:\u001b[0m 644 rows x 6 columns                                                      │\n",
       "╰──────────────────────────────────────────────────────────────────────────────────╯\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "data = fetch_dataset(name='fuel_consumption', raw=True)\n",
    "data = data[['Fecha', 'Gasolinas']]\n",
    "data = data.rename(columns={'Fecha':'date', 'Gasolinas':'litters'})\n",
    "data['date'] = pd.to_datetime(data['date'], format='%Y-%m-%d')\n",
    "data = data.set_index('date')\n",
    "data = data.loc[:'1990-01-01 00:00:00']\n",
    "data = data.asfreq('MS')\n",
    "data = data['litters']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pmdarima"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Performing stepwise search to minimize aic\n",
      " ARIMA(0,1,0)(1,1,1)[12]             : AIC=5585.000, Time=0.57 sec\n",
      " ARIMA(0,1,0)(0,1,0)[12]             : AIC=5659.651, Time=0.05 sec\n",
      " ARIMA(1,1,0)(1,1,0)[12]             : AIC=5503.446, Time=0.91 sec\n",
      " ARIMA(0,1,1)(0,1,1)[12]             : AIC=5483.497, Time=1.55 sec\n",
      " ARIMA(0,1,1)(0,1,0)[12]             : AIC=5543.713, Time=0.23 sec\n",
      " ARIMA(0,1,1)(1,1,1)[12]             : AIC=5485.158, Time=1.01 sec\n",
      " ARIMA(0,1,1)(0,1,2)[12]             : AIC=5484.830, Time=2.21 sec\n",
      " ARIMA(0,1,1)(1,1,0)[12]             : AIC=5507.997, Time=0.75 sec\n",
      " ARIMA(0,1,1)(1,1,2)[12]             : AIC=5484.222, Time=5.58 sec\n",
      " ARIMA(0,1,0)(0,1,1)[12]             : AIC=5587.917, Time=0.28 sec\n",
      " ARIMA(1,1,1)(0,1,1)[12]             : AIC=5469.858, Time=0.92 sec\n",
      " ARIMA(1,1,1)(0,1,0)[12]             : AIC=5530.808, Time=0.19 sec\n",
      " ARIMA(1,1,1)(1,1,1)[12]             : AIC=5471.444, Time=0.99 sec\n",
      " ARIMA(1,1,1)(0,1,2)[12]             : AIC=5471.105, Time=1.83 sec\n",
      " ARIMA(1,1,1)(1,1,0)[12]             : AIC=5491.140, Time=0.71 sec\n",
      " ARIMA(1,1,1)(1,1,2)[12]             : AIC=5470.191, Time=4.58 sec\n",
      " ARIMA(1,1,0)(0,1,1)[12]             : AIC=5481.126, Time=0.40 sec\n",
      " ARIMA(2,1,1)(0,1,1)[12]             : AIC=5471.613, Time=1.47 sec\n",
      " ARIMA(1,1,2)(0,1,1)[12]             : AIC=5471.240, Time=1.06 sec\n",
      " ARIMA(0,1,2)(0,1,1)[12]             : AIC=5472.211, Time=0.56 sec\n",
      " ARIMA(2,1,0)(0,1,1)[12]             : AIC=5472.646, Time=0.49 sec\n",
      " ARIMA(2,1,2)(0,1,1)[12]             : AIC=5472.893, Time=1.80 sec\n",
      " ARIMA(1,1,1)(0,1,1)[12] intercept   : AIC=5471.748, Time=0.80 sec\n",
      "\n",
      "Best model:  ARIMA(1,1,1)(0,1,1)[12]          \n",
      "Total fit time: 29.014 seconds\n"
     ]
    }
   ],
   "source": [
    "warnings.filterwarnings(\"ignore\", message=\".*force_all_finite.*\", category=FutureWarning)\n",
    "modelo = auto_arima(\n",
    "            y                 = data,\n",
    "            start_p           = 0,\n",
    "            start_q           = 0,\n",
    "            max_p             = 3,\n",
    "            max_q             = 3,\n",
    "            seasonal          = True,\n",
    "            test              = 'adf',\n",
    "            m                 = 12,   # periodicidad de la estacionalidad\n",
    "            d                 = None, # El algoritmo determina 'd'\n",
    "            D                 = None, # El algoritmo determina 'D'\n",
    "            trace             = True,\n",
    "            error_action      = 'ignore',\n",
    "            suppress_warnings = True,\n",
    "            stepwise          = True\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom AutoARIMA following hydman et al. (2022)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from statsmodels.tsa.stattools import adfuller, kpss\n",
    "from statsmodels.tsa.arima.model import ARIMA as SMARIMA\n",
    "\n",
    "\n",
    "def ndiffs_adf(y, alpha=0.05, max_d=2):\n",
    "    \"\"\"Estimate number of differences using ADF test (H0: unit root = non-stationary).\"\"\"\n",
    "\n",
    "    for i in range(max_d + 1):\n",
    "        try:\n",
    "            pvalue = adfuller(np.diff(y, i), autolag=\"AIC\")[1]\n",
    "        except Exception:\n",
    "            # Fall back if test fails (short/constant series)\n",
    "            return i if i > 0 else 0\n",
    "        if pvalue < alpha:\n",
    "            return i\n",
    "    return max_d\n",
    "\n",
    "def ndiffs_kpss(y, alpha=0.05, max_d=2):\n",
    "    \"\"\"Estimate number of differences using KPSS test (H0: stationary).\"\"\"\n",
    "\n",
    "    for i in range(max_d + 1):\n",
    "        try:\n",
    "            with warnings.catch_warnings():\n",
    "                warnings.simplefilter(\"ignore\")\n",
    "                _, pvalue, *_ = kpss(np.diff(y, i), nlags=\"auto\")\n",
    "        except Exception:\n",
    "            # Fall back if test fails (short/constant series)\n",
    "            return i if i > 0 else 0\n",
    "        if pvalue >= alpha:\n",
    "            return i\n",
    "    return max_d\n",
    "\n",
    "def estimate_d(y, max_d=2):\n",
    "    \"\"\"Combine ADF and KPSS heuristically (Hyndman 2008).\"\"\"\n",
    "    try:\n",
    "        adf_d = ndiffs_adf(y, max_d=max_d)\n",
    "        kpss_d = ndiffs_kpss(y, max_d=max_d)\n",
    "        return max(adf_d, kpss_d)\n",
    "    except Exception:\n",
    "        return 1  # safe fallback\n",
    "\n",
    "\n",
    "def auto_arima_hyndman(\n",
    "    y,\n",
    "    p_max=5,\n",
    "    q_max=5,\n",
    "    max_d=2,\n",
    "    seasonal=False,\n",
    "    verbose=True,\n",
    "    maxiter=100,\n",
    "):\n",
    "    \"\"\"\n",
    "    Fast stepwise Auto-ARIMA (non-seasonal).\n",
    "    - Stepwise neighbors + occasional drop-to-zero moves.\n",
    "    - Uses AICc for selection.\n",
    "    \"\"\"\n",
    "    if seasonal:\n",
    "        raise NotImplementedError(\"Seasonal search not implemented in this fast version.\")\n",
    "\n",
    "    y_arr = y.to_numpy() if isinstance(y, pd.Series) else np.asarray(y, dtype=float)\n",
    "    n = len(y_arr)\n",
    "    if n < 10:\n",
    "        warnings.warn(\"Series is very short; results may be unstable.\")\n",
    "    d = estimate_d(y_arr, max_d=max_d)\n",
    "\n",
    "    # Trend rule: include constant for d in {0,1}, else no constant\n",
    "    if d == 0:\n",
    "        trend = \"c\"\n",
    "    elif d == 1:\n",
    "        trend = \"t\"\n",
    "    else:\n",
    "        trend = \"n\"\n",
    "\n",
    "    # Search state\n",
    "    tried = {}\n",
    "    def fit_try(p, q):\n",
    "\n",
    "        if not (0 <= p <= p_max and 0 <= q <= q_max):\n",
    "            return None, np.inf\n",
    "        key = (p, d, q, trend)\n",
    "        if key in tried:\n",
    "            return tried[key]\n",
    "        try:\n",
    "            model = SMARIMA(\n",
    "                y_arr,\n",
    "                order=(p, d, q),\n",
    "                trend=trend,\n",
    "                enforce_stationarity=False,\n",
    "                enforce_invertibility=False,\n",
    "            )\n",
    "            res = model.fit(method_kwargs={\"warn_convergence\": False, \"maxiter\": maxiter})\n",
    "            aicc = res.aic\n",
    "            tried[key] = (res, aicc)\n",
    "            return res, aicc\n",
    "        except Exception as e:\n",
    "            print(f\"Failed to fit ARIMA({p},{d},{q}) with trend='{trend}': {e}\")\n",
    "            tried[key] = (None, np.inf)\n",
    "            return None, np.inf\n",
    "\n",
    "    # Start from (1, d, 1) but clamp to ranges\n",
    "    p = min(max(1, 0), p_max)\n",
    "    q = min(max(1, 0), q_max)\n",
    "\n",
    "    best_res, best_aicc = fit_try(p, q)\n",
    "    best_order = (p, d, q)\n",
    "    if verbose and np.isfinite(best_aicc):\n",
    "        print(f\"Start: AICc={best_aicc:.2f}, order={best_order}\")\n",
    "\n",
    "    improved = True\n",
    "    while improved:\n",
    "        improved = False\n",
    "        candidates = []\n",
    "\n",
    "        # Neighbor moves\n",
    "        for dp, dq in [(1,0), (-1,0), (0,1), (0,-1)]:\n",
    "            np_, nq_ = p + dp, q + dq\n",
    "            res, aicc = fit_try(np_, nq_)\n",
    "            candidates.append((aicc, (np_, d, nq_), res))\n",
    "\n",
    "        # Occasional drop-to-zero moves (fast exploration)\n",
    "        if p > 0:\n",
    "            res, aicc = fit_try(0, q)\n",
    "            candidates.append((aicc, (0, d, q), res))\n",
    "        if q > 0:\n",
    "            res, aicc = fit_try(p, 0)\n",
    "            candidates.append((aicc, (p, d, 0), res))\n",
    "\n",
    "        # Pick best among candidates\n",
    "        if candidates:\n",
    "            aicc, order, res = min(candidates, key=lambda x: x[0])\n",
    "            if aicc + 1e-6 < best_aicc:\n",
    "                best_aicc = aicc\n",
    "                best_order = order\n",
    "                best_res = res\n",
    "                p, _, q = order\n",
    "                improved = True\n",
    "                if verbose:\n",
    "                    print(f\"Improved: AICc={aicc:.2f}, order={order}\")\n",
    "\n",
    "    if verbose:\n",
    "        print(f\"Best model: AICc={best_aicc:.2f}, order={best_order}\")\n",
    "    return best_res, best_order, best_aicc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Start: AICc=6177.17, order=(1, 2, 1)\n",
      "Improved: AICc=6157.34, order=(1, 2, 2)\n",
      "Improved: AICc=6132.00, order=(1, 2, 3)\n",
      "Improved: AICc=6113.75, order=(2, 2, 3)\n",
      "Improved: AICc=6035.78, order=(2, 2, 4)\n",
      "Improved: AICc=5973.42, order=(2, 2, 5)\n",
      "Best model: AICc=5973.42, order=(2, 2, 5)\n"
     ]
    }
   ],
   "source": [
    "model, order, aicc = auto_arima_hyndman(data, p_max=5, q_max=5, max_d=2, seasonal=False, verbose=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom AutoARIMA with bayesian search and statsmodels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import optuna\n",
    "from statsmodels.tsa.arima.model import ARIMA\n",
    "import warnings\n",
    "from functools import partial\n",
    "\n",
    "def estimate_d(y, max_d=2, alpha=0.05):\n",
    "    \"\"\"\n",
    "    Estimate minimal non-seasonal differencing order d using ADF+KPSS.\n",
    "    \"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    for d in range(max_d + 1):\n",
    "        diff_y = np.diff(y, n=d)\n",
    "        if len(diff_y) < 10:\n",
    "            break\n",
    "        try:\n",
    "            with warnings.catch_warnings():\n",
    "                warnings.simplefilter(\"ignore\")\n",
    "                adf_p = adfuller(diff_y, autolag=\"AIC\")[1]\n",
    "                kpss_p = kpss(diff_y, nlags=\"auto\")[1]\n",
    "            if adf_p < alpha and kpss_p > alpha:\n",
    "                return d\n",
    "        except Exception:\n",
    "            continue\n",
    "    warnings.warn(f\"No stationary differencing found up to d={max_d}. Using d={max_d}.\")\n",
    "    \n",
    "    return max_d\n",
    "\n",
    "\n",
    "def estimate_D(y, m, max_D=1, alpha=0.05):\n",
    "    \"\"\"\n",
    "    Estimate seasonal differencing order D (0 or 1) using KPSS.\n",
    "    \"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    for D in range(max_D + 1):\n",
    "        if D == 0:\n",
    "            diff_y = y\n",
    "        else:\n",
    "            diff_y = y[m:] - y[:-m]\n",
    "        if len(diff_y) < 10:\n",
    "            break\n",
    "        try:\n",
    "            with warnings.catch_warnings():\n",
    "                warnings.simplefilter(\"ignore\")\n",
    "                kpss_p = kpss(diff_y, nlags=\"auto\")[1]\n",
    "            if kpss_p > alpha:\n",
    "                return D\n",
    "        except Exception:\n",
    "            continue\n",
    "    return max_D\n",
    "\n",
    "class BayesianAutoArimaStatsModels:\n",
    "    \"\"\"\n",
    "    Auto-ARIMA using Bayesian Optimization (via Optuna).\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    p_max : int, default=5\n",
    "        Maximum autoregressive (AR) order to search.\n",
    "    d_max : int or None, default=None\n",
    "        Maximum differencing order to search. If None, estimate using ADF+KPSS.\n",
    "    q_max : int, default=5\n",
    "        Maximum moving-average (MA) order to search.\n",
    "    P_max : int, default=0\n",
    "        Maximum seasonal autoregressive (SAR) order to search.\n",
    "    D_max : int or None, default=None\n",
    "        Maximum seasonal differencing order to search. If None, estimate using KPSS.\n",
    "    Q_max : int, default=0\n",
    "        Maximum seasonal moving-average (SMA) order to search.\n",
    "    m : int, default=0\n",
    "        Seasonal period.\n",
    "    criterion : {\"aic\", \"aicc\", \"bic\"}, default=\"aic\"\n",
    "        Information criterion to minimize.\n",
    "    suggested_trials : list, default=None\n",
    "        List of parameter dicts to enqueue as initial trials. For example:\n",
    "            [{'p':1, 'd':1, 'q':1, 'P':0, 'D':1, 'Q':0},\n",
    "             {'p':0, 'd':1, 'q':1, 'P':0, 'D':1, 'Q':0},\n",
    "             {'p':2, 'd':1, 'q':2, 'P':0, 'D':1, 'Q':0}]\n",
    "    early_stopping_rounds : int, default=10\n",
    "        Stop optimization if no improvement in this many trials.\n",
    "    random_state : int or None\n",
    "        Random seed for reproducibility.\n",
    "    verbose : bool, default=True\n",
    "        If True, show progress messages.\n",
    "    show_progress_bar : bool, default=True\n",
    "        If True, display Optuna's progress bar.\n",
    "\n",
    "    Attributes\n",
    "    ----------\n",
    "    best_model_ : ARIMAResults\n",
    "        Fitted ARIMA model with best found parameters.\n",
    "    best_params_ : dict\n",
    "        Best (p,d,q) parameters found.\n",
    "    study_ : optuna.study.Study\n",
    "        Optuna study object after optimization.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        p_max=5,\n",
    "        q_max=5,\n",
    "        d_max=2,\n",
    "        P_max=2,\n",
    "        Q_max=2,\n",
    "        D_max=1,\n",
    "        m=0,\n",
    "        use_stat_tests=False,\n",
    "        criterion=\"aic\",\n",
    "        suggested_trials=None,\n",
    "        early_stopping_rounds=10,\n",
    "        random_state=None,\n",
    "        verbose=True,\n",
    "        show_progress_bar=False,\n",
    "    ):\n",
    "        self.p_max = p_max\n",
    "        self.d_max = d_max\n",
    "        self.q_max = q_max\n",
    "        self.P_max = P_max\n",
    "        self.D_max = D_max\n",
    "        self.Q_max = Q_max\n",
    "        self.m = m\n",
    "        self.use_stat_tests = use_stat_tests\n",
    "        self.criterion = criterion\n",
    "        self.suggested_trials = suggested_trials\n",
    "        self.early_stopping_rounds = early_stopping_rounds\n",
    "        self.random_state = random_state\n",
    "        self.verbose = verbose\n",
    "        self.show_progress_bar = show_progress_bar\n",
    "\n",
    "        self.d_ = None\n",
    "        self.D_ = None\n",
    "        self.best_model_ = None\n",
    "        self.best_params_ = None\n",
    "        self.study_ = None\n",
    "\n",
    "        total_combinations = (\n",
    "            max(1, self.p_max) * max(1, self.q_max) * max(1, self.P_max) * max(1, self.Q_max)\n",
    "        )\n",
    "        if self.use_stat_tests:\n",
    "            total_combinations += 2\n",
    "        else:\n",
    "            total_combinations = total_combinations * max(1, self.d_max) * max(1, self.D_max)\n",
    "        self.n_trials = total_combinations\n",
    "\n",
    "    def _criterion_value(self, res, n):\n",
    "        \"\"\"\n",
    "        Compute information criterion.\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        res : ARIMAResults\n",
    "            Fitted ARIMA model result.\n",
    "        n : int\n",
    "            Number of observations.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        float\n",
    "            Value of the selected information criterion.\n",
    "        \"\"\"\n",
    "        if self.criterion == \"aic\":\n",
    "            return res.aic\n",
    "        elif self.criterion == \"bic\":\n",
    "            return res.bic\n",
    "        elif self.criterion == \"aicc\":\n",
    "            k = res.params.size\n",
    "            return res.aic + (2 * k * (k + 1)) / (n - k - 1)\n",
    "        else:\n",
    "            raise ValueError(\"criterion must be 'aic', 'aicc', or 'bic'\")\n",
    "\n",
    "    def fit(self, y):\n",
    "        \"\"\"\n",
    "        Run Bayesian optimization to select best (p,d,q).\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        y : array-like of shape (n_samples,)\n",
    "            Univariate time series data.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        None\n",
    "        \"\"\"\n",
    "        y_arr = y.to_numpy() if isinstance(y, pd.Series) else y\n",
    "        n = len(y_arr)\n",
    "\n",
    "        if self.use_stat_tests:\n",
    "            D = 0\n",
    "            if self.m > 1:\n",
    "                D = estimate_D(y_arr, m=self.m, max_D=self.D_max)\n",
    "            self.D_ = D\n",
    "\n",
    "            if D > 0:\n",
    "                y_diff = y_arr[self.m:] - y_arr[:-self.m]\n",
    "            else:\n",
    "                y_diff = y_arr\n",
    "            d = estimate_d(y_diff, max_d=self.d_max)\n",
    "            self.d_ = d\n",
    "\n",
    "        def objective_broad(trial):\n",
    "            p = trial.suggest_int(\"p\", 0, self.p_max)\n",
    "            q = trial.suggest_int(\"q\", 0, self.q_max)\n",
    "\n",
    "            if self.m > 1:\n",
    "                P = trial.suggest_int(\"P\", 0, self.P_max)\n",
    "                Q = trial.suggest_int(\"Q\", 0, self.Q_max)\n",
    "            else:\n",
    "                P = trial.suggest_int(\"P\", 0, 0)\n",
    "                Q = trial.suggest_int(\"Q\", 0, 0)\n",
    "\n",
    "            if self.use_stat_tests:\n",
    "                d = trial.suggest_categorical(\"d\", [self.d_])\n",
    "                D = trial.suggest_categorical(\"D\", [self.D_])\n",
    "            else:\n",
    "                d = trial.suggest_int(\"d\", 0, self.d_max)\n",
    "                D = trial.suggest_int(\"D\", 0, self.D_max)\n",
    "\n",
    "            m = trial.suggest_int(\"m\", self.m, self.m)\n",
    "            \n",
    "            try:\n",
    "                order = (p, d, q)\n",
    "                seasonal_order = (P, D, Q, m) if m > 1 else (0, 0, 0, 0)\n",
    "                model = ARIMA(y_arr, order=order, seasonal_order=seasonal_order)\n",
    "                with warnings.catch_warnings():\n",
    "                    warnings.simplefilter(\"ignore\")\n",
    "                    res = model.fit()\n",
    "                objective_value = self._criterion_value(res, n)\n",
    "                if self.verbose:\n",
    "                    print(f\"ARIMA({p},{d},{q}) ({P},{D},{Q},{self.m}) : {self.criterion.upper()}={objective_value:.4f}\")\n",
    "                return objective_value\n",
    "            except Exception as e:\n",
    "                if self.verbose:\n",
    "                    print(f\"Trial ({p},{d},{q}) ({P},{D},{Q},{self.m}): {e}\")\n",
    "                return np.inf\n",
    "        \n",
    "        def objective_fine(trial):\n",
    "            p = trial.suggest_int(\"p\", max(0, best_trial.params[\"p\"] - 1), min(self.p_max, best_trial.params[\"p\"] + 1))\n",
    "            q = trial.suggest_int(\"q\", max(0, best_trial.params[\"q\"] - 1), min(self.q_max, best_trial.params[\"q\"] + 1))\n",
    "            \n",
    "            if self.m > 1:\n",
    "                P = trial.suggest_int(\"P\", max(0, best_trial.params[\"P\"] - 1), min(self.P_max, best_trial.params[\"P\"] + 1))\n",
    "                Q = trial.suggest_int(\"Q\", max(0, best_trial.params[\"Q\"] - 1), min(self.Q_max, best_trial.params[\"Q\"] + 1))\n",
    "            else:\n",
    "                P = 0\n",
    "                Q = 0\n",
    "\n",
    "            if self.use_stat_tests:\n",
    "                d = trial.suggest_categorical(\"d\", [self.d_])\n",
    "                D = trial.suggest_categorical(\"D\", [self.D_])\n",
    "            else:\n",
    "                d = trial.suggest_int(\"d\", max(0, best_trial.params[\"d\"] - 1), min(self.d_max, best_trial.params[\"d\"] + 1))\n",
    "                D = trial.suggest_int(\"D\", max(0, best_trial.params[\"D\"] - 1), min(self.D_max, best_trial.params[\"D\"] + 1))\n",
    "\n",
    "            m = trial.suggest_int(\"m\", self.m, self.m)      \n",
    "            \n",
    "            try:\n",
    "                order = (p, d, q)\n",
    "                seasonal_order = (P, D, Q, m) if m > 1 else (0, 0, 0, 0)\n",
    "                model = ARIMA(y_arr, order=order, seasonal_order=seasonal_order)\n",
    "                with warnings.catch_warnings():\n",
    "                    warnings.simplefilter(\"ignore\")\n",
    "                    res = model.fit()\n",
    "                objective_value = self._criterion_value(res, n)\n",
    "                if self.verbose:\n",
    "                    print(f\"ARIMA({p},{d},{q}) ({P},{D},{Q},{self.m}) : {self.criterion.upper()}={objective_value:.4f}\")\n",
    "                return objective_value\n",
    "            except Exception as e:\n",
    "                if self.verbose:\n",
    "                    print(f\"Trial ({p},{d},{q}) ({P},{D},{Q},{self.m}): {e}\")\n",
    "                return np.inf\n",
    "            \n",
    "        def _early_stop_callback(study, trial, early_stopping_rounds, label=\"\"):\n",
    "            if study.best_trial.number == trial.number:\n",
    "                study.set_user_attr(\"last_improvement\", trial.number)\n",
    "                return\n",
    "            last = study.user_attrs.get(\"last_improvement\", trial.number)\n",
    "            if trial.number - last >= early_stopping_rounds:\n",
    "                if self.verbose:\n",
    "                    print(f\"⏹️  Early stopping in {label} after {early_stopping_rounds} trials without improvement.\")\n",
    "                study.stop()\n",
    "\n",
    "\n",
    "        # First search phase: broad exploration\n",
    "        optuna.logging.set_verbosity(optuna.logging.WARNING)\n",
    "        study = optuna.create_study(\n",
    "            direction=\"minimize\",\n",
    "            sampler=optuna.samplers.TPESampler(seed=self.random_state),\n",
    "        )\n",
    "\n",
    "        if self.suggested_trials is not None:\n",
    "            for trial_params in self.suggested_trials:\n",
    "                study.enqueue_trial(trial_params)\n",
    "\n",
    "        if self.verbose:\n",
    "            print(f\"🔍 Starting Bayesian Auto-ARIMA search to minimize {self.criterion}\")\n",
    "        \n",
    "        study.optimize(\n",
    "            objective_broad,\n",
    "            n_trials=self.n_trials,\n",
    "            show_progress_bar=self.show_progress_bar,\n",
    "            callbacks=[partial(_early_stop_callback, early_stopping_rounds=self.early_stopping_rounds, label=\"broad-search\")]\n",
    "        )\n",
    "        \n",
    "        # Second search phase: fine-tune around best found\n",
    "        best_trial = study.best_trial\n",
    "        study = optuna.create_study(\n",
    "            direction=\"minimize\",\n",
    "            sampler=optuna.samplers.TPESampler(seed=self.random_state),\n",
    "        )\n",
    "        study.optimize(\n",
    "            objective_fine,\n",
    "            n_trials=self.n_trials,\n",
    "            show_progress_bar=self.show_progress_bar,\n",
    "            callbacks=[partial(_early_stop_callback, early_stopping_rounds=self.early_stopping_rounds, label=\"fine-tune\")]\n",
    "        )\n",
    "\n",
    "        self.study_ = study\n",
    "        self.best_params_ = study.best_params\n",
    "        best_order = (self.best_params_[\"p\"], self.best_params_[\"d\"], self.best_params_[\"q\"])\n",
    "\n",
    "        # Refit best model\n",
    "        with warnings.catch_warnings():\n",
    "            warnings.simplefilter(\"ignore\")\n",
    "            order = (self.best_params_[\"p\"], self.best_params_[\"d\"], self.best_params_[\"q\"])\n",
    "            seasonal_order = (self.best_params_[\"P\"], self.best_params_[\"D\"], self.best_params_[\"Q\"], self.m) if self.m > 1 else (0, 0, 0, 0)\n",
    "            self.best_model_ = ARIMA(y_arr, order=order, seasonal_order=seasonal_order).fit()\n",
    "        if self.verbose:\n",
    "            print(f\"✅ Best model: ARIMA({order})({seasonal_order}) with {self.criterion}={self._criterion_value(self.best_model_, n):.4f}\")\n",
    "\n",
    "        return\n",
    "\n",
    "    def predict(self, n_periods=1):\n",
    "        \"\"\"Forecast n_periods ahead.\"\"\"\n",
    "        if self.best_model_ is None:\n",
    "            raise RuntimeError(\"Model not fitted yet. Call .fit(y) first.\")\n",
    "        return self.best_model_.forecast(steps=n_periods)\n",
    "\n",
    "    def summary(self):\n",
    "        \"\"\"Return the summary of the best ARIMA model.\"\"\"\n",
    "        if self.best_model_ is None:\n",
    "            raise RuntimeError(\"Model not fitted yet.\")\n",
    "        return self.best_model_.summary()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Mejor modelo con pdmarima auto_arima\n",
    "# ARIMA(1,1,1)(0,1,1)[12]             : AIC=5469.858"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🔍 Starting Bayesian Auto-ARIMA search to minimize aic\n",
      "ARIMA(1,1,1) (1,1,1,12) : AIC=5471.4443\n",
      "ARIMA(0,1,1) (0,0,0,12) : AIC=6208.1778\n",
      "ARIMA(0,2,0) (1,1,0,12) : AIC=5888.8932\n",
      "ARIMA(0,2,1) (0,0,0,12) : AIC=6204.5860\n",
      "ARIMA(0,2,3) (1,0,0,12) : AIC=6109.9121\n",
      "ARIMA(2,0,2) (1,1,0,12) : AIC=5539.2562\n",
      "ARIMA(3,0,2) (0,1,1,12) : AIC=5499.5224\n",
      "ARIMA(0,2,2) (1,0,0,12) : AIC=6089.1052\n",
      "ARIMA(1,0,3) (0,1,1,12) : AIC=5501.8480\n",
      "ARIMA(3,2,3) (0,0,1,12) : AIC=6090.4644\n",
      "ARIMA(2,1,0) (1,1,1,12) : AIC=5474.3170\n",
      "⏹️  Early stopping in broad-search after 10 trials without improvement.\n",
      "ARIMA(1,1,0) (1,0,1,12) : AIC=6025.5529\n",
      "ARIMA(2,2,2) (0,0,0,12) : AIC=6211.1224\n",
      "ARIMA(1,2,1) (1,0,1,12) : AIC=6054.6108\n",
      "ARIMA(2,2,1) (0,0,0,12) : AIC=6198.1350\n",
      "ARIMA(2,2,2) (0,1,1,12) : AIC=5458.9552\n",
      "ARIMA(0,2,1) (1,0,1,12) : AIC=6005.5691\n",
      "ARIMA(0,1,1) (0,0,0,12) : AIC=6208.1778\n",
      "ARIMA(0,2,0) (1,1,0,12) : AIC=5888.8932\n",
      "ARIMA(2,0,2) (1,1,1,12) : AIC=5518.0561\n",
      "ARIMA(2,2,2) (1,0,0,12) : AIC=6085.0112\n",
      "ARIMA(1,0,2) (0,1,1,12) : AIC=5527.3326\n",
      "ARIMA(2,0,2) (0,1,1,12) : AIC=5517.0656\n",
      "ARIMA(2,0,2) (0,1,1,12) : AIC=5517.0656\n",
      "ARIMA(2,1,2) (0,1,1,12) : AIC=5472.8926\n",
      "ARIMA(1,1,2) (0,1,1,12) : AIC=5471.2403\n",
      "⏹️  Early stopping in fine-tune after 10 trials without improvement.\n",
      "✅ Best model: ARIMA((2, 2, 2))((0, 1, 1, 12)) with aic=5458.9552\n"
     ]
    }
   ],
   "source": [
    "# study.enqueue_trial({\"p\": 1, \"d\": self.d_, \"q\": 1, \"P\": 0, \"D\": self.D_, \"Q\": 0, 'm':self.m})\n",
    "        # study.enqueue_trial({\"p\": 0, \"d\": self.d_, \"q\": 1, \"P\": 0, \"D\": self.D_, \"Q\": 0, 'm':self.m})\n",
    "        # study.enqueue_trial({\"p\": 2, \"d\": self.d_, \"q\": 2, \"P\": 0, \"D\": self.D_, \"Q\": 0, 'm':self.m})\n",
    "autoarima = BayesianAutoArimaStatsModels(\n",
    "    p_max=3,\n",
    "    d_max=2,\n",
    "    q_max=3,\n",
    "    D_max=1,\n",
    "    P_max=1,\n",
    "    Q_max=1,\n",
    "    m=12,\n",
    "    use_stat_tests=False,\n",
    "    early_stopping_rounds=10,\n",
    "    suggested_trials=[\n",
    "        {'p':1, 'd':1, 'q':1, 'P':1, 'D':1, 'Q':1, 'm':12},\n",
    "        {'p':0, 'd':1, 'q':1, 'P':0, 'D':0, 'Q':0, 'm':12},\n",
    "    ],\n",
    "    verbose=True,\n",
    "    show_progress_bar=False\n",
    ")\n",
    "autoarima.fit(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom AutoARIMA with bayesian search and aeon"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import optuna\n",
    "from aeon.forecasting.stats import ARIMA as AeonARIMA\n",
    "import warnings\n",
    "from functools import partial\n",
    "\n",
    "def estimate_d(y, max_d=2, alpha=0.05):\n",
    "    \"\"\"\n",
    "    Estimate minimal non-seasonal differencing order d using ADF+KPSS.\n",
    "    \"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    for d in range(max_d + 1):\n",
    "        diff_y = np.diff(y, n=d)\n",
    "        if len(diff_y) < 10:\n",
    "            break\n",
    "        try:\n",
    "            with warnings.catch_warnings():\n",
    "                warnings.simplefilter(\"ignore\")\n",
    "                adf_p = adfuller(diff_y, autolag=\"AIC\")[1]\n",
    "                kpss_p = kpss(diff_y, nlags=\"auto\")[1]\n",
    "            if adf_p < alpha and kpss_p > alpha:\n",
    "                return d\n",
    "        except Exception:\n",
    "            continue\n",
    "    warnings.warn(f\"No stationary differencing found up to d={max_d}. Using d={max_d}.\")\n",
    "    \n",
    "    return max_d\n",
    "\n",
    "\n",
    "def estimate_D(y, m, max_D=1, alpha=0.05):\n",
    "    \"\"\"\n",
    "    Estimate seasonal differencing order D (0 or 1) using KPSS.\n",
    "    \"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    for D in range(max_D + 1):\n",
    "        if D == 0:\n",
    "            diff_y = y\n",
    "        else:\n",
    "            diff_y = y[m:] - y[:-m]\n",
    "        if len(diff_y) < 10:\n",
    "            break\n",
    "        try:\n",
    "            with warnings.catch_warnings():\n",
    "                warnings.simplefilter(\"ignore\")\n",
    "                kpss_p = kpss(diff_y, nlags=\"auto\")[1]\n",
    "            if kpss_p > alpha:\n",
    "                return D\n",
    "        except Exception:\n",
    "            continue\n",
    "    return max_D\n",
    "\n",
    "class BayesianAutoArimaAeon:\n",
    "    \"\"\"\n",
    "    Auto-ARIMA using Bayesian Optimization (via Optuna).\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    p_max : int, default=5\n",
    "        Maximum autoregressive (AR) order to search.\n",
    "    d_max : int or None, default=None\n",
    "        Maximum differencing order to search. If None, estimate using ADF+KPSS.\n",
    "    q_max : int, default=5\n",
    "        Maximum moving-average (MA) order to search.\n",
    "    P_max : int, default=0\n",
    "        Maximum seasonal autoregressive (SAR) order to search.\n",
    "    D_max : int or None, default=None\n",
    "        Maximum seasonal differencing order to search. If None, estimate using KPSS.\n",
    "    Q_max : int, default=0\n",
    "        Maximum seasonal moving-average (SMA) order to search.\n",
    "    m : int, default=0\n",
    "        Seasonal period.\n",
    "    criterion : {\"aic\", \"aicc\", \"bic\"}, default=\"aic\"\n",
    "        Information criterion to minimize.\n",
    "    suggested_trials : list, default=None\n",
    "        List of parameter dicts to enqueue as initial trials. For example:\n",
    "            [{'p':1, 'd':1, 'q':1},\n",
    "             {'p':0, 'd':1, 'q':1}]\n",
    "    early_stopping_rounds : int, default=10\n",
    "        Stop optimization if no improvement in this many trials.\n",
    "    random_state : int or None\n",
    "        Random seed for reproducibility.\n",
    "    verbose : bool, default=True\n",
    "        If True, show progress messages.\n",
    "    show_progress_bar : bool, default=True\n",
    "        If True, display Optuna's progress bar.\n",
    "\n",
    "    Attributes\n",
    "    ----------\n",
    "    best_model_ : ARIMAResults\n",
    "        Fitted ARIMA model with best found parameters.\n",
    "    best_params_ : dict\n",
    "        Best (p,d,q) parameters found.\n",
    "    study_ : optuna.study.Study\n",
    "        Optuna study object after optimization.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        p_max=5,\n",
    "        q_max=5,\n",
    "        d_max=2,\n",
    "        P_max=2,\n",
    "        Q_max=2,\n",
    "        D_max=1,\n",
    "        m=0,\n",
    "        use_stat_tests=False,\n",
    "        criterion=\"aic\",\n",
    "        suggested_trials=None,\n",
    "        early_stopping_rounds=10,\n",
    "        random_state=None,\n",
    "        verbose=True,\n",
    "        show_progress_bar=False,\n",
    "    ):\n",
    "        self.p_max = p_max\n",
    "        self.d_max = d_max\n",
    "        self.q_max = q_max\n",
    "        self.P_max = P_max\n",
    "        self.D_max = D_max\n",
    "        self.Q_max = Q_max\n",
    "        self.m = m\n",
    "        self.use_stat_tests = use_stat_tests\n",
    "        self.criterion = criterion\n",
    "        self.suggested_trials = suggested_trials\n",
    "        self.early_stopping_rounds = early_stopping_rounds\n",
    "        self.random_state = random_state\n",
    "        self.verbose = verbose\n",
    "        self.show_progress_bar = show_progress_bar\n",
    "\n",
    "        self.d_ = None\n",
    "        self.D_ = None\n",
    "        self.best_model_ = None\n",
    "        self.best_params_ = None\n",
    "        self.study_ = None\n",
    "\n",
    "        total_combinations = (\n",
    "            max(1, self.p_max) * max(1, self.q_max) * max(1, self.P_max) * max(1, self.Q_max)\n",
    "        )\n",
    "        if self.use_stat_tests:\n",
    "            total_combinations += 2\n",
    "        else:\n",
    "            total_combinations = total_combinations * max(1, self.d_max) * max(1, self.D_max)\n",
    "        self.n_trials_ = total_combinations\n",
    "\n",
    "    def fit(self, y):\n",
    "        \"\"\"\n",
    "        Run Bayesian optimization to select best (p,d,q).\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        y : array-like of shape (n_samples,)\n",
    "            Univariate time series data.\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        None\n",
    "        \"\"\"\n",
    "        y_arr = y.to_numpy() if isinstance(y, pd.Series) else y\n",
    "        n = len(y_arr)\n",
    "\n",
    "        if self.use_stat_tests:\n",
    "            D = 0\n",
    "            if self.m > 1:\n",
    "                D = estimate_D(y_arr, m=self.m, max_D=self.D_max)\n",
    "            self.D_ = D\n",
    "\n",
    "            if D > 0:\n",
    "                y_diff = y_arr[self.m:] - y_arr[:-self.m]\n",
    "            else:\n",
    "                y_diff = y_arr\n",
    "            d = estimate_d(y_diff, max_d=self.d_max)\n",
    "            self.d_ = d\n",
    "\n",
    "        def objective_broad(trial):\n",
    "            p = trial.suggest_int(\"p\", 0, self.p_max)\n",
    "            q = trial.suggest_int(\"q\", 0, self.q_max)\n",
    "\n",
    "            # if self.m > 1:\n",
    "            #     P = trial.suggest_int(\"P\", 0, self.P_max)\n",
    "            #     Q = trial.suggest_int(\"Q\", 0, self.Q_max)\n",
    "            # else:\n",
    "            #     P = trial.suggest_int(\"P\", 0, 0)\n",
    "            #     Q = trial.suggest_int(\"Q\", 0, 0)\n",
    "\n",
    "            if self.use_stat_tests:\n",
    "                d = trial.suggest_categorical(\"d\", [self.d_])\n",
    "                # D = trial.suggest_categorical(\"D\", [self.D_])\n",
    "            else:\n",
    "                d = trial.suggest_int(\"d\", 0, self.d_max)\n",
    "                # D = trial.suggest_int(\"D\", 0, self.D_max)\n",
    "\n",
    "            # m = trial.suggest_int(\"m\", self.m, self.m)\n",
    "            \n",
    "            try:\n",
    "                # order = (p, d, q)\n",
    "                # seasonal_order = (P, D, Q, m) if m > 1 else (0, 0, 0, 0)\n",
    "                model = AeonARIMA(p=p, d=d, q=q)\n",
    "                with warnings.catch_warnings():\n",
    "                    warnings.simplefilter(\"ignore\")\n",
    "                    model.fit(y=y)\n",
    "                objective_value = model.aic_\n",
    "                if self.verbose:\n",
    "                    print(f\"ARIMA({p},{d},{q}) ({0},{0},{0},{self.m}) : {self.criterion.upper()}={objective_value:.4f}\")\n",
    "            except Exception as e:\n",
    "                if self.verbose:\n",
    "                    print(f\"Trial ({p},{d},{q}) ({0},{0},{0},{self.m}): {e}\")\n",
    "                objective_value = np.inf\n",
    "            \n",
    "            return objective_value\n",
    "        \n",
    "        def objective_fine(trial):\n",
    "            p = trial.suggest_int(\"p\", max(0, best_trial.params[\"p\"] - 1), min(self.p_max, best_trial.params[\"p\"] + 1))\n",
    "            q = trial.suggest_int(\"q\", max(0, best_trial.params[\"q\"] - 1), min(self.q_max, best_trial.params[\"q\"] + 1))\n",
    "            \n",
    "            # if self.m > 1:\n",
    "            #     P = trial.suggest_int(\"P\", max(0, best_trial.params[\"P\"] - 1), min(self.P_max, best_trial.params[\"P\"] + 1))\n",
    "            #     Q = trial.suggest_int(\"Q\", max(0, best_trial.params[\"Q\"] - 1), min(self.Q_max, best_trial.params[\"Q\"] + 1))\n",
    "            # else:\n",
    "            #     P = 0\n",
    "            #     Q = 0\n",
    "\n",
    "            if self.use_stat_tests:\n",
    "                d = trial.suggest_categorical(\"d\", [self.d_])\n",
    "                # D = trial.suggest_categorical(\"D\", [self.D_])\n",
    "            else:\n",
    "                d = trial.suggest_int(\"d\", max(0, best_trial.params[\"d\"] - 1), min(self.d_max, best_trial.params[\"d\"] + 1))\n",
    "                # D = trial.suggest_int(\"D\", max(0, best_trial.params[\"D\"] - 1), min(self.D_max, best_trial.params[\"D\"] + 1))\n",
    "\n",
    "            # m = trial.suggest_int(\"m\", self.m, self.m)      \n",
    "            \n",
    "            try:\n",
    "                # order = (p, d, q)\n",
    "                # seasonal_order = (P, D, Q, m) if m > 1 else (0, 0, 0, 0)\n",
    "                model = AeonARIMA(p=p, d=d, q=q)\n",
    "                with warnings.catch_warnings():\n",
    "                    warnings.simplefilter(\"ignore\")\n",
    "                    model.fit(y)\n",
    "                objective_value = model.aic_\n",
    "                if self.verbose:\n",
    "                    print(f\"ARIMA({p},{d},{q}) ({0},{0},{0},{self.m}) : {self.criterion.upper()}={objective_value:.4f}\")\n",
    "            except Exception as e:\n",
    "                if self.verbose:\n",
    "                    print(f\"Trial ({p},{d},{q}) ({0},{0},{0},{self.m}): {e}\")\n",
    "                objective_value = np.inf\n",
    "                \n",
    "            return objective_value\n",
    "            \n",
    "        def _early_stop_callback(study, trial, early_stopping_rounds, label=\"\"):\n",
    "            if study.best_trial.number == trial.number:\n",
    "                study.set_user_attr(\"last_improvement\", trial.number)\n",
    "                return\n",
    "            last = study.user_attrs.get(\"last_improvement\", trial.number)\n",
    "            if trial.number - last >= early_stopping_rounds:\n",
    "                if self.verbose:\n",
    "                    print(f\"⏹️  Early stopping in {label} after {early_stopping_rounds} trials without improvement.\")\n",
    "                study.stop()\n",
    "\n",
    "\n",
    "        # First search phase: broad exploration\n",
    "        optuna.logging.set_verbosity(optuna.logging.WARNING)\n",
    "        study = optuna.create_study(\n",
    "            direction=\"minimize\",\n",
    "            sampler=optuna.samplers.TPESampler(seed=self.random_state),\n",
    "        )\n",
    "        \n",
    "        if self.suggested_trials is not None:\n",
    "            for trial_params in self.suggested_trials:\n",
    "                study.enqueue_trial(trial_params)\n",
    "\n",
    "        if self.verbose:\n",
    "            print(f\"🔍 Starting Bayesian Auto-ARIMA search to minimize {self.criterion}\")\n",
    "        \n",
    "        study.optimize(\n",
    "            objective_broad,\n",
    "            n_trials=self.n_trials_,\n",
    "            show_progress_bar=self.show_progress_bar,\n",
    "            callbacks=[partial(_early_stop_callback, early_stopping_rounds=self.early_stopping_rounds, label=\"broad-search\")]\n",
    "        )\n",
    "        \n",
    "        # Second search phase: fine-tune around best found\n",
    "        best_trial = study.best_trial\n",
    "        study = optuna.create_study(\n",
    "            direction=\"minimize\",\n",
    "            sampler=optuna.samplers.TPESampler(seed=self.random_state),\n",
    "        )\n",
    "        study.optimize(\n",
    "            objective_fine,\n",
    "            n_trials=self.n_trials_,\n",
    "            show_progress_bar=self.show_progress_bar,\n",
    "            callbacks=[partial(_early_stop_callback, early_stopping_rounds=self.early_stopping_rounds, label=\"fine-search\")]\n",
    "        )\n",
    "\n",
    "        self.study_ = study\n",
    "        self.best_params_ = study.best_params\n",
    "        self.best_order_ = (self.best_params_[\"p\"], self.best_params_[\"d\"], self.best_params_[\"q\"])\n",
    "        # self.best_seasonal_order_ = (self.best_params_[\"P\"], self.best_params_[\"D\"], self.best_params_[\"Q\"], self.m) if self.m > 1 else (0, 0, 0, 0)\n",
    "\n",
    "        # Refit best model\n",
    "        with warnings.catch_warnings():\n",
    "            warnings.simplefilter(\"ignore\")\n",
    "            self.best_model_ = AeonARIMA(p=self.best_order_[0], d=self.best_order_[1], q=self.best_order_[2]).fit(y)\n",
    "        if self.verbose:\n",
    "            print(f\"✅ Best model: ARIMA({self.best_order_})() with {self.criterion}={self.best_model_.aic_:.4f}\")\n",
    "\n",
    "        return\n",
    "\n",
    "    def predict(self, n_periods=1):\n",
    "        \"\"\"Forecast n_periods ahead.\"\"\"\n",
    "        if self.best_model_ is None:\n",
    "            raise RuntimeError(\"Model not fitted yet. Call .fit(y) first.\")\n",
    "        return self.best_model_.forecast(steps=n_periods)\n",
    "\n",
    "    def summary(self):\n",
    "        \"\"\"Return the summary of the best ARIMA model.\"\"\"\n",
    "        if self.best_model_ is None:\n",
    "            raise RuntimeError(\"Model not fitted yet.\")\n",
    "        return self.best_model_.summary()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🔍 Starting Bayesian Auto-ARIMA search to minimize aic\n",
      "ARIMA(1,1,1) (0,0,0,0) : AIC=6177.3925\n",
      "ARIMA(0,1,1) (0,0,0,0) : AIC=6205.8148\n",
      "ARIMA(3,1,2) (0,0,0,0) : AIC=6124.4584\n",
      "ARIMA(0,0,2) (0,0,0,0) : AIC=6776.2044\n",
      "ARIMA(2,1,2) (0,0,0,0) : AIC=6153.0577\n",
      "ARIMA(0,1,0) (0,0,0,0) : AIC=6212.3363\n",
      "ARIMA(1,1,3) (0,0,0,0) : AIC=6171.1550\n",
      "ARIMA(1,0,1) (0,0,0,0) : AIC=6231.3989\n",
      "⏹️  Early stopping in broad-search after 5 trials without improvement.\n",
      "ARIMA(3,2,2) (0,0,0,0) : AIC=6184.9252\n",
      "ARIMA(3,0,2) (0,0,0,0) : AIC=6234.0209\n",
      "ARIMA(3,2,1) (0,0,0,0) : AIC=6272.0233\n",
      "ARIMA(3,2,1) (0,0,0,0) : AIC=6272.0233\n",
      "ARIMA(2,0,3) (0,0,0,0) : AIC=6195.3756\n",
      "ARIMA(2,2,1) (0,0,0,0) : AIC=6187.1939\n",
      "⏹️  Early stopping in fine-search after 5 trials without improvement.\n",
      "✅ Best model: ARIMA((3, 2, 2))() with aic=6184.9252\n"
     ]
    }
   ],
   "source": [
    "autoarima = BayesianAutoArimaAeon(\n",
    "    p_max=3,\n",
    "    d_max=2,\n",
    "    q_max=3,\n",
    "    D_max=0,\n",
    "    P_max=0, \n",
    "    Q_max=0,\n",
    "    m=0,\n",
    "    use_stat_tests=False,\n",
    "    suggested_trials=[\n",
    "        {'p':1, 'd':1, 'q':1},\n",
    "        {'p':0, 'd':1, 'q':1},\n",
    "    ],\n",
    "    early_stopping_rounds=5,\n",
    "    verbose=True,\n",
    "    show_progress_bar=False\n",
    ")\n",
    "autoarima.fit(data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "6145.565923976397\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  display: none;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  display: block;\n",
       "  width: 100%;\n",
       "  overflow: visible;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".estimator-table summary {\n",
       "    padding: .5rem;\n",
       "    font-family: monospace;\n",
       "    cursor: pointer;\n",
       "}\n",
       "\n",
       ".estimator-table details[open] {\n",
       "    padding-left: 0.1rem;\n",
       "    padding-right: 0.1rem;\n",
       "    padding-bottom: 0.3rem;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table {\n",
       "    margin-left: auto !important;\n",
       "    margin-right: auto !important;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:nth-child(odd) {\n",
       "    background-color: #fff;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:nth-child(even) {\n",
       "    background-color: #f6f6f6;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:hover {\n",
       "    background-color: #e0e0e0;\n",
       "}\n",
       "\n",
       ".estimator-table table td {\n",
       "    border: 1px solid rgba(106, 105, 104, 0.232);\n",
       "}\n",
       "\n",
       ".user-set td {\n",
       "    color:rgb(255, 94, 0);\n",
       "    text-align: left;\n",
       "}\n",
       "\n",
       ".user-set td.value pre {\n",
       "    color:rgb(255, 94, 0) !important;\n",
       "    background-color: transparent !important;\n",
       "}\n",
       "\n",
       ".default td {\n",
       "    color: black;\n",
       "    text-align: left;\n",
       "}\n",
       "\n",
       ".user-set td i,\n",
       ".default td i {\n",
       "    color: black;\n",
       "}\n",
       "\n",
       ".copy-paste-icon {\n",
       "    background-image: url();\n",
       "    background-repeat: no-repeat;\n",
       "    background-size: 14px 14px;\n",
       "    background-position: 0;\n",
       "    display: inline-block;\n",
       "    width: 14px;\n",
       "    height: 14px;\n",
       "    cursor: pointer;\n",
       "}\n",
       "</style><body><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>ARIMA(d=1, p=np.int64(3), q=np.int64(3), use_constant=np.True_)</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>ARIMA</div></div><div><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\" data-param-prefix=\"\">\n",
       "        <div class=\"estimator-table\">\n",
       "            <details>\n",
       "                <summary>Parameters</summary>\n",
       "                <table class=\"parameters-table\">\n",
       "                  <tbody>\n",
       "                    \n",
       "        <tr class=\"user-set\">\n",
       "            <td><i class=\"copy-paste-icon\"\n",
       "                 onclick=\"copyToClipboard('p',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></i></td>\n",
       "            <td class=\"param\">p&nbsp;</td>\n",
       "            <td class=\"value\">np.int64(3)</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"user-set\">\n",
       "            <td><i class=\"copy-paste-icon\"\n",
       "                 onclick=\"copyToClipboard('d',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></i></td>\n",
       "            <td class=\"param\">d&nbsp;</td>\n",
       "            <td class=\"value\">1</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"user-set\">\n",
       "            <td><i class=\"copy-paste-icon\"\n",
       "                 onclick=\"copyToClipboard('q',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></i></td>\n",
       "            <td class=\"param\">q&nbsp;</td>\n",
       "            <td class=\"value\">np.int64(3)</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"user-set\">\n",
       "            <td><i class=\"copy-paste-icon\"\n",
       "                 onclick=\"copyToClipboard('use_constant',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></i></td>\n",
       "            <td class=\"param\">use_constant&nbsp;</td>\n",
       "            <td class=\"value\">np.True_</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><i class=\"copy-paste-icon\"\n",
       "                 onclick=\"copyToClipboard('iterations',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></i></td>\n",
       "            <td class=\"param\">iterations&nbsp;</td>\n",
       "            <td class=\"value\">200</td>\n",
       "        </tr>\n",
       "    \n",
       "                  </tbody>\n",
       "                </table>\n",
       "            </details>\n",
       "        </div>\n",
       "    </div></div></div></div></div><script>function copyToClipboard(text, element) {\n",
       "    // Get the parameter prefix from the closest toggleable content\n",
       "    const toggleableContent = element.closest('.sk-toggleable__content');\n",
       "    const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';\n",
       "    const fullParamName = paramPrefix ? `${paramPrefix}${text}` : text;\n",
       "\n",
       "    const originalStyle = element.style;\n",
       "    const computedStyle = window.getComputedStyle(element);\n",
       "    const originalWidth = computedStyle.width;\n",
       "    const originalHTML = element.innerHTML.replace('Copied!', '');\n",
       "\n",
       "    navigator.clipboard.writeText(fullParamName)\n",
       "        .then(() => {\n",
       "            element.style.width = originalWidth;\n",
       "            element.style.color = 'green';\n",
       "            element.innerHTML = \"Copied!\";\n",
       "\n",
       "            setTimeout(() => {\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 2000);\n",
       "        })\n",
       "        .catch(err => {\n",
       "            console.error('Failed to copy:', err);\n",
       "            element.style.color = 'red';\n",
       "            element.innerHTML = \"Failed!\";\n",
       "            setTimeout(() => {\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 2000);\n",
       "        });\n",
       "    return false;\n",
       "}\n",
       "\n",
       "document.querySelectorAll('.fa-regular.fa-copy').forEach(function(element) {\n",
       "    const toggleableContent = element.closest('.sk-toggleable__content');\n",
       "    const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';\n",
       "    const paramName = element.parentElement.nextElementSibling.textContent.trim();\n",
       "    const fullParamName = paramPrefix ? `${paramPrefix}${paramName}` : paramName;\n",
       "\n",
       "    element.setAttribute('title', fullParamName);\n",
       "});\n",
       "</script></body>"
      ],
      "text/plain": [
       "ARIMA(d=1, p=np.int64(3), q=np.int64(3), use_constant=np.True_)"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "autoarima = AutoARIMA(max_p=3, max_d=2, max_q=3)\n",
    "autoarima.fit(data)\n",
    "print(autoarima.final_model_.aic_)\n",
    "autoarima.final_model_"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "skforecast_19_py13",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.9"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
